﻿namespace SuperModel.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    /// <summary>
    /// Extensions for System.Type instances
    /// </summary>
    public static class TypeExtensions
    {
        /// <summary>
        /// Determines if a Type implements another Type
        /// </summary>
        /// <param name="type">
        /// The type to check.
        /// </param>
        /// <param name="typeToImplement">
        /// The type to implement.
        /// </param>
        /// <returns>
        /// A value indicating whether or not the type implements the target type
        /// </returns>
        public static bool Implements(this Type type, Type typeToImplement)
        {
            return typeToImplement.IsAssignableFrom(type);
        }

        /// <summary>
        /// Assert that a Type implements a Target Type
        /// </summary>
        /// <param name="type">
        /// The type to check.
        /// </param>
        /// <param name="typeToImplement">
        /// The type to implement.
        /// </param>
        public static void AssertImplements(this Type type, Type typeToImplement)
        {
            string errorMessage = String.Format("{0} must implement {1}.", type, typeToImplement);
            AssertImplements(type, typeToImplement, errorMessage);
        }

        /// <summary>
        /// Assert that a type implements a target type
        /// </summary>
        /// <param name="type">
        /// The type to check.
        /// </param>
        /// <param name="typeToImplement">
        /// The type to implement.
        /// </param>
        /// <param name="message">
        /// The message used in the exception if the type does not implement the target type.
        /// </param>
        /// <exception cref="ArgumentException">
        /// Thrown if the type does not implement the target type
        /// </exception>
        public static void AssertImplements(this Type type, Type typeToImplement, string message)
        {
            if (!type.Implements(typeToImplement))
            {
                throw new ArgumentException(message);
            }
        }

        /// <summary>
        /// Find attributes over all properties on a type and its type hierarchy.
        /// </summary>
        /// <param name="type">
        /// The type to check.
        /// </param>
        /// <typeparam name="TAttribute">
        /// The attribute type to find
        /// </typeparam>
        /// <returns>
        /// The found attributes.
        /// </returns>
        public static IDictionary<string, IList<TAttribute>> FindAttributesDeepCheckProperties<TAttribute>(this Type type)
        {
            var results = new Dictionary<string, IList<TAttribute>>();

            var inspectionList = GetTypeInspectionList(type);
            foreach (var innerType in inspectionList)
            {
                var propertiesInType = innerType.GetProperties();
                foreach (var property in propertiesInType)
                {
                    var attributes = property.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();

                    if (!results.ContainsKey(property.Name))
                    {
                        results[property.Name] = new List<TAttribute>();
                    }

                    // Add the attributes to the named list.
                    ((List<TAttribute>)results[property.Name]).AddRange(attributes);
                }
            }

            // Reverse the Lists
            var returnMe = new Dictionary<string, IList<TAttribute>>();
            foreach (var kvp in results)
            {
                returnMe[kvp.Key] = new List<TAttribute>(kvp.Value.Reverse());
            }

            return returnMe;
        }

        /// <summary>
        /// Inspects a type for behavior attributes
        /// </summary>
        /// <param name="typeToSearch">
        /// The type to search.
        /// </param>
        /// <typeparam name="TAttribute">
        /// The attribute type to find
        /// </typeparam>
        /// <returns>
        /// The found attributes
        /// </returns>
        public static IList<TAttribute> FindAttributesDeep<TAttribute>(this Type typeToSearch)
            where TAttribute : Attribute
        {
            return FindAttributes<TAttribute>(GetTypeInspectionList(typeToSearch), typeof(TAttribute));
        }

        /// <summary>
        /// Finds all attributes on a type
        /// </summary>
        /// <param name="typeToSearch">
        /// The type to search.
        /// </param>
        /// <returns>
        /// The list of attributes
        /// </returns>
        public static IList<Attribute> FindAllAttributesDeep(this Type typeToSearch)
        {
            return FindAttributes<Attribute>(GetTypeInspectionList(typeToSearch), null);
        }

        /// <summary>
        /// Find attributes over a list of types
        /// </summary>
        /// <param name="inspectionList">
        /// The inspection list.
        /// </param>
        /// <param name="attributeType">
        /// The attribute type.
        /// </param>
        /// <typeparam name="TAttribute">
        /// The attribute type
        /// </typeparam>
        /// <returns>
        /// The found attributes
        /// </returns>
        private static IList<TAttribute> FindAttributes<TAttribute>(IEnumerable<Type> inspectionList, Type attributeType)
            where TAttribute : Attribute
        {
            var returnMe = new List<TAttribute>();
            foreach (Type type in inspectionList)
            {
                // Get the attributes on the current type
                var attributesOnTypeToSearch = (attributeType == null ?
                                                type.GetCustomAttributes(false) :
                                                type.GetCustomAttributes(typeof(TAttribute), false)).Cast<TAttribute>();

                // Add the attributes to the found attribute list
                returnMe.AddRange(attributesOnTypeToSearch);
            }

            return returnMe;
        }

        /// <summary>
        /// Gets a Type Inspection List
        /// </summary>
        /// <param name="root">
        /// The root type.
        /// </param>
        /// <returns>
        /// The type inspection List
        /// </returns>
        private static IEnumerable<Type> GetTypeInspectionList(Type root)
        {
            var returnMe = new List<Type> { root };
            
            // Inspect the base class if appropriate.
            if (!root.IsInterface && root.BaseType != null)
            {
                returnMe.AddRange(GetTypeInspectionList(root.BaseType));
            }

            // Inspect the interfaces of the type
            foreach (var type in root.GetInterfaces())
            {
                returnMe.AddRange(GetTypeInspectionList(type));
            }

            return returnMe.Distinct();
        }
    }
}